/**
 * Copyright (c) 2012-2013 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation.
 * Oracle and Java are trademarks or registered trademarks of Oracle and/or its
 * affiliates. Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners.
 * See LICENSE.TXT for license information.
 */

package com.nokia.example.amaze.ui;

import java.util.Enumeration;
import java.util.Vector;

import javax.microedition.lcdui.Image;
import javax.microedition.m3g.Appearance;
import javax.microedition.m3g.Background;
import javax.microedition.m3g.CompositingMode;
import javax.microedition.m3g.Image2D;
import javax.microedition.m3g.Mesh;
import javax.microedition.m3g.Node;
import javax.microedition.m3g.PolygonMode;
import javax.microedition.m3g.Texture2D;
import javax.microedition.m3g.Transform;
import javax.microedition.m3g.World;

import com.nokia.example.amaze.Main;
import com.nokia.example.amaze.model.MarbleModel;
import com.nokia.example.amaze.model.Maze;

/**
 * Helper class containing methods to create the world content. This
 * functionality is separated into its own class so that MazeCanvas
 * wouldn't be so full.
 */
public class WorldBuilder {
    // Constants
    private static final int MAX_NODES_TO_DESTROY_COUNT = 70;
    
    // Filenames of graphical assets
    private static final String BACKGROUND_IMAGE_FILENAME = "/graphics/background.jpg";
    private static final String MARBLE_IMAGE_FILENAME = "/graphics/marble.png";
    private static final String FLOOR_IMAGE_FILENAME = "/graphics/floor.png";
    private static final String WALL_IMAGE_FILENAME = "/graphics/wall.png";
    private static final String GOAL_IMAGE_FILENAME = "/graphics/goal.png";

    // Members
    private Node[] _nodesToDestroy = null;

    /**
     * Constructor.
     */
    public WorldBuilder() {
        _nodesToDestroy = new Node[MAX_NODES_TO_DESTROY_COUNT];
    }

    /**
     * Sets up the maze.
     * @param world
     * @param maze
     * @param wallAppearance
     * @param wallClearAppearance
     */
    public void createNewMaze(World world,
                              Maze maze,
                              Appearance wallAppearance)
    {
        // Destroy the previous maze if one exists
        destroyMaze(world);
        
        // Generate a new maze
        maze.createNew(MazeCanvas.MAZE_CORRIDOR_COUNT);
        
        // Create the planes based on the generated maze
        Enumeration wallsEnum = createPlanes(maze);
        
        // Release resources not needed anymore
        maze.clear();
        
        // Create meshes of the planes and add them to the world
        while (wallsEnum.hasMoreElements()) {
            Mesh wallMesh = ((Plane)wallsEnum.nextElement()).createMesh();
            wallMesh.setAppearance(0, wallAppearance);
            world.addChild(wallMesh);
            addNodeToDestroy(wallMesh);
        }
        
        // Create the goal mark to the end of the maze
        createGoalMark(world, maze);
    }

    /**
     * Creates the background.
     * @param world
     * @return The created background instance.
     */
    public Background createBackground(World world) {
        Background background = new Background();
        Image backgroundImage = Main.makeImage(BACKGROUND_IMAGE_FILENAME);
        
        if (backgroundImage != null) {
            background.setImage(new Image2D(Image2D.RGB, backgroundImage));
            background.setImageMode(Background.REPEAT, Background.REPEAT);
            world.setBackground(background);
        }
        
        return background;
    }

    /**
     * Creates the wall appearances.
     * @param wallAppearance
     * @param wallClearAppearance
     */
    public void createWallAppearances(Appearance wallAppearance,
                                      Appearance wallClearAppearance)
    {
        // The walls need perspective correction enabled
        PolygonMode wallPolygonMode = new PolygonMode();
        wallPolygonMode.setPerspectiveCorrectionEnable(true);
        
        // Build the wall semi-transparent appearance
        wallClearAppearance.setPolygonMode(wallPolygonMode);
        
        // This is to make a wall semi-transparent
        CompositingMode wallClearCompositeMode = new CompositingMode();
        wallClearCompositeMode.setBlending(CompositingMode.ALPHA_ADD);
        wallClearAppearance.setCompositingMode(wallClearCompositeMode);
        
        // Build the normal wall appearance
        wallAppearance.setPolygonMode(wallPolygonMode);
        
        // Load the wall texture
        Image wallTextureImage = Main.makeImage(WALL_IMAGE_FILENAME);
        
        if (wallTextureImage != null) {
            Texture2D wallTexture = null;
            wallTexture = new Texture2D(new Image2D(Image2D.RGB, wallTextureImage));
            
            // The texture is repeated
            wallTexture.setWrapping(Texture2D.WRAP_REPEAT,
                                    Texture2D.WRAP_REPEAT);//Texture2D.WRAP_CLAMP);
            wallTexture.setBlending(Texture2D.FUNC_REPLACE);
            wallTexture.setFiltering(Texture2D.FILTER_LINEAR,
                                     Texture2D.FILTER_NEAREST);
            // Set the texture
            wallAppearance.setTexture(0, wallTexture);
            wallClearAppearance.setTexture(0, wallTexture);
        }
    }

    /**
     * Creates the floor.
     * @param world
     */
    public void createFloor(World world) {
        float floorSide = MazeCanvas.MAZE_SIDE_LENGTH / 2;
        
        // define the location and size of the floor
        Transform floorTransform = new Transform();
        floorTransform.postRotate(90.0f, -1.0f, 0.0f, 0.0f);
        floorTransform.postScale(floorSide, floorSide, 1.0f);
        
        // The floor appearance. Basically a texture repeated many times
        Appearance floorAppearance = new Appearance();
        
        // The floor needs that perspective correction is enabled
        PolygonMode floorPolygonMode = new PolygonMode();
        floorPolygonMode.setPerspectiveCorrectionEnable(true);
        floorAppearance.setPolygonMode(floorPolygonMode);
        
        // Load the texture
        Texture2D floorTexture = null;
        Image floorTextureImage = Main.makeImage(FLOOR_IMAGE_FILENAME);
        
        if (floorTextureImage != null) {
            floorTexture = new Texture2D(
              new Image2D(Image2D.RGB, floorTextureImage));
            
            // the texture is repeated many times
            floorTexture.setWrapping(Texture2D.WRAP_REPEAT,
                                     Texture2D.WRAP_REPEAT);
            floorTexture.setBlending(Texture2D.FUNC_REPLACE);
            floorTexture.setFiltering(Texture2D.FILTER_LINEAR,
                                      Texture2D.FILTER_NEAREST);
            floorAppearance.setTexture(0, floorTexture);
        }
        
        Plane floor = new Plane(floorTransform, 10);
        
        // Build the mesh
        Mesh floorMesh = floor.createMesh();
        floorMesh.setAppearance(0, floorAppearance);
        
        // The floor is not pickable
        floorMesh.setPickingEnable(false);
        
        // Add to the world
        world.addChild(floorMesh);
    }

    /**
     * Creates the goal mark.
     * @param world
     * @param maze
     */
    private void createGoalMark(World world, Maze maze) {
        Appearance appearance = new Appearance();
        
        CompositingMode compositingMode = new CompositingMode();
        compositingMode.setBlending(CompositingMode.ALPHA);
        appearance.setCompositingMode(compositingMode);
        
        Texture2D texture = null;
        Image textureImage = Main.makeImage(GOAL_IMAGE_FILENAME);
        
        if (textureImage != null) {
            texture = new Texture2D(
              new Image2D(Image2D.RGBA, textureImage));
            
            // The texture is not repeated
            texture.setWrapping(Texture2D.WRAP_CLAMP,
                                Texture2D.WRAP_CLAMP);
            texture.setBlending(Texture2D.FUNC_REPLACE);
            texture.setFiltering(Texture2D.FILTER_NEAREST,
                                 Texture2D.FILTER_NEAREST);
            appearance.setTexture(0, texture);
        }
        
        // Create the goal mesh
        Plane goalMarkPlane = createGoalMark(maze);
        Mesh goalMarkMesh = goalMarkPlane.createMesh();
        goalMarkMesh.setAppearance(0, appearance);
        goalMarkMesh.setPickingEnable(false); // Not pickable
        
        world.addChild(goalMarkMesh);
        addNodeToDestroy(goalMarkMesh);
    }

    /**
     * Creates the marble.
     * @param world
     * @return The newly created marble as a Mesh instance.
     */
    public Mesh createMarble(World world) {
        Transform transform = new Transform();
        transform.postRotate(90.0f, -1.0f, 0.0f, 0.0f);
        transform.postScale(MarbleModel.DEFAULT_SIZE,
                            MarbleModel.DEFAULT_SIZE, 1.0f);
        
        Appearance appearance = new Appearance();
        CompositingMode compositingMode = new CompositingMode();
        compositingMode.setBlending(CompositingMode.ALPHA);
        appearance.setCompositingMode(compositingMode);
        
        Texture2D texture = null;
        Image marbleImage = Main.makeImage(MARBLE_IMAGE_FILENAME);
        
        if (marbleImage != null) {
            texture = new Texture2D(new Image2D(Image2D.RGBA, marbleImage));
            
            // The texture is not repeated
            texture.setWrapping(Texture2D.WRAP_CLAMP,
                                Texture2D.WRAP_CLAMP);
            texture.setBlending(Texture2D.FUNC_REPLACE);
            texture.setFiltering(Texture2D.FILTER_NEAREST,
                                 Texture2D.FILTER_NEAREST);
            appearance.setTexture(0, texture);
        }
        
        Plane marblePlane = new Plane(transform, 1);
        Mesh marble = marblePlane.createMesh();
        marble.setAppearance(0, appearance);
        marble.setRenderingEnable(false);
        marble.setPickingEnable(false);
        
        world.addChild(marble);
        
        return marble;
    }

    /**
     * Destroys the maze planes and goal mark.
     * @param world The world from which to remove the nodes.
     */
    public void destroyMaze(World world) {
        if (_nodesToDestroy != null) {
            for (int i = 0; i < MAX_NODES_TO_DESTROY_COUNT; ++i) {
                if (_nodesToDestroy[i] != null) {
                    world.removeChild(_nodesToDestroy[i]);
                }
            }
            
            _nodesToDestroy = null;
        }
    }

    /**
     * Adds a reference of the given node to the internal array so that it can
     * be removed from the world instance and deleted later (when a new maze
     * needs to be generated).
     * @param node The node to destroy later.
     */
    private boolean addNodeToDestroy(Node node) {
        if (_nodesToDestroy == null) {
            _nodesToDestroy = new Node[MAX_NODES_TO_DESTROY_COUNT];
            _nodesToDestroy[0] = node;
            return true;
        }
        
        for (int i = 0; i < MAX_NODES_TO_DESTROY_COUNT; ++i) {
            if (_nodesToDestroy[i] == null) {
                _nodesToDestroy[i] = node;
                return true;
            }
        }
        
        // The array is full
        System.out.println("WorldBuilder::addNodeToDestroy(): The array is full!");
        return false;
    }

    /**
     * Creates a plane located at the end of the maze.
     * @return The goal plane.
     */
    private Plane createGoalMark(Maze maze) {
        Transform markTransform = new Transform();
        markTransform.postTranslate(maze.origin() + maze.goalX()
                                    * maze.spaceBetweenPlanes(),
                                    maze.height() / 2 + 0.2f,
                                    -maze.origin() - 5f);
        markTransform.postScale(10f, 10f, 10f);
        markTransform.postRotate(90f, -1f, 0f, 0f);
        return new Plane(markTransform, 1f);
    }

    /**
     * Creates the horizontal and vertical planes and puts them in an
     * enumeration. Note that after calling this method the maze array becomes
     * unusable as it is released (set to null).
     * @param maze The model of the maze.
     * @return The enumeration of the components in the planes vector.
     */
    private Enumeration createPlanes(Maze maze) {
        long[] mazeArray = maze.array();
        
        if (mazeArray == null || mazeArray.length == 0) {
            return null;
        }
        
        float spaceBetweenPlanes = maze.spaceBetweenPlanes();
        float mazeOrigin = maze.origin();
        float mazeHeight = maze.height();
        Vector allPlanes = new Vector();
        
        for (int i = 0; i < mazeArray.length; i++) {
            int startX = -1;
            
            for (int j = 0; j < mazeArray.length; j++) {
                long shift = (0x1L << j);
                
                if ((mazeArray[i] & shift) == shift && startX == -1) {
                    startX = j;
                    continue;
                }
                
                if ((((mazeArray[i] & shift) == 0) || (j == (mazeArray.length - 1)))
                        && (startX >= 0))
                {
                    int steps = j - startX;
                    
                    // Don't create walls of side 1 since they will be created
                    // on the other direction
                    if (steps == 1) {
                        startX = -1;
                        continue;
                    }
                    
                    // compensate that the last item is always 1
                    if (j == (mazeArray.length - 1)) {
                        steps++;
                    }
                    
                    Transform planeTransform = new Transform();
                    
                    // Divided by 2 since the original square is of side 2
                    float wallWidth = (maze.spaceBetweenPlanes() * (steps - 1) / 2);
                    
                    // Move to the correct position
                    planeTransform.postTranslate(
                            mazeOrigin + spaceBetweenPlanes * startX + wallWidth,
                            mazeHeight, mazeOrigin + spaceBetweenPlanes * i);
                    
                    // Give the actual size
                    planeTransform.postScale(wallWidth, mazeHeight, 1f);
                    allPlanes.addElement(new Plane(planeTransform, 1f));
                    startX = -1;
                }
            }
        }
        
        for (int i = 0; i < mazeArray.length; i++) {
            int startY = -1;
            long shift = (0x1L << i);
            
            for (int j = 0; j < mazeArray.length; j++) {
                if ((mazeArray[j] & shift) == shift && startY == -1) {
                    startY = j;
                    continue;
                }
                
                if ((((mazeArray[j] & shift) == 0) || (j == (mazeArray.length - 1)))
                        && (startY >= 0))
                {
                    int steps = j - startY;
                    
                    if (steps == 1) {
                        startY = -1;
                        continue;
                    }
                    
                    if (j == (mazeArray.length - 1)) {
                        steps++;
                    }
                    
                    Transform planeTransform = new Transform();
                    
                    // Divided by 2 since the original square is of side 2
                    float wallWidth = (spaceBetweenPlanes * (steps - 1) / 2);
                    
                    // Translate to the correct position
                    planeTransform.postTranslate(
                            mazeOrigin + spaceBetweenPlanes * i, mazeHeight,
                            mazeOrigin + spaceBetweenPlanes * startY + wallWidth);
                    
                    // Rotate 90 degrees since this is a vertical wall
                    planeTransform.postRotate(90f, 0f, 1f, 0f);
                    
                    // Give the correct size
                    planeTransform.postScale(wallWidth, mazeHeight, 1f);
                    allPlanes.addElement(new Plane(planeTransform, 1f));
                    startY = -1;
                }
            }
        }
        
        return allPlanes.elements();
    }
}
